import * as fs from 'fs';
import {graphql, GraphQLScalarType, GraphQLSchema, Kind} from 'graphql';
import {introspectionQuery} from 'graphql/utilities';
import {introspectSchema, makeRemoteExecutableSchema} from 'graphql-tools';
import {HttpLink} from 'apollo-link-http';
import * as fetch from 'node-fetch';
import {ApolloLink} from 'apollo-link';
import {GQC, TypeComposer} from 'graphql-compose';
import {Resolver} from 'graphql-compose';
import {ExecutionResult} from 'graphql/execution/execute';
import {GraphQLInputObjectType} from 'graphql-compose/lib/graphql';
import Decimal from 'decimal.js';

export const GraphQLDate = new GraphQLScalarType({
  name: 'Date',
  description: '日期/时间类型',
  serialize: String,
  parseValue(value: any) {
    if (typeof value !== 'string') {
      throw new TypeError('Field error: value is an invalid Date');
    }
    return value;
  },
  parseLiteral(ast) {
    return ast.kind === Kind.STRING ? ast.value : null;
  }
});

export const GraphQLDecimal = new GraphQLScalarType({
  name: 'Decimal',
  description: 'Decimal类型',
  serialize: String,
  parseValue(value: any) {
    return value ? new Decimal(value) : 0;
  },
  parseLiteral(ast) {
    return ast.kind === Kind.STRING || ast.kind === Kind.INT || ast.kind === Kind.FLOAT ? ast.value : null;
  }
});

GQC['typeMapper'].set('Date', GraphQLDate);
GQC['typeMapper'].set('Decimal', GraphQLDecimal);

export async function getRemoteSchema(uri: string): Promise<GraphQLSchema> {
  try {
    const customFetch = (uri, options) => {
      return fetch(uri, options);
    };
    const middlewareLink = new ApolloLink((operation, forward) => {
      const context = operation.getContext();
      const req = context.graphqlContext;
      if (req && req.headers) {
        operation.setContext({graphqlContext: context.graphqlContext, headers: {cookie: req.headers['cookie']}});
      }
      return forward(operation).map(result => {
        const context = operation.getContext();
        const response = context.response;
        const responseHeaders = response.headers._headers;

        if (req && responseHeaders) {
          const headers = {};
          const excludeHeaderNames = ['connection', 'content-encoding'];
          for (const headerName in responseHeaders) {
            if (
              responseHeaders[headerName] &&
              responseHeaders[headerName].length > 0 &&
              excludeHeaderNames.indexOf(headerName) === -1
            ) {
              headers[headerName] = responseHeaders[headerName][0];
            }
          }
          for (const headerName in headers) {
            req.res.header(headerName, headers[headerName]);
          }
        }

        return result;
      });
    });
    const httpLink = new HttpLink({uri, fetch: customFetch});

    const link = middlewareLink.concat(<any>httpLink);

    const schema = await introspectSchema(<any>link);
    const remoteSchema = makeRemoteExecutableSchema({
      schema: <any>schema,
      link: <any>link
    });
    return remoteSchema;
  } catch (err) {
    console.error(err);
  }
}

export async function getJsonSchema(schema: GraphQLSchema): Promise<ExecutionResult> {
  const jsonSchema = await graphql(schema, introspectionQuery);
  return jsonSchema;
}

export async function generateJsonFileBySchema(schema: GraphQLSchema, generateDirectory: string): Promise<void> {
  const jsonSchema = await graphql(schema, introspectionQuery);
  if (!fs.existsSync(generateDirectory)) {
    fs.mkdirSync(generateDirectory);
  }
  const schemaPath = generateDirectory + '/schema.json';
  fs.writeFileSync(schemaPath, JSON.stringify(jsonSchema, null, 2));
}

export async function generateJsonFileByUrl(graphQLUrl: string, generateDirectory: string): Promise<void> {
  const schema = await getRemoteSchema(graphQLUrl);
  await generateJsonFileBySchema(schema, generateDirectory);
}

export function createResultMetaFields(codeDescription: string = '0.成功; 1.失败'): any {
  return {
    errCode: {
      description: codeDescription,
      type: 'Int',
      resolve: parent => parent.errCode || 0
    },
    errText: {
      description: '错误描述',
      type: 'String',
      resolve: parent => parent.errText || ''
    }
  };
}

export function createTypeComposer(config: any): TypeComposer<any> {
  const tc = TypeComposer.create(config.name);
  tc.setDescription(config.description || '');
  tc.addFields(config.fields);
  return tc;
}

export function createInputTypeComposer(config: any): GraphQLInputObjectType {
  const tc = TypeComposer.create(config.name);
  tc.setDescription(config.description || '');
  tc.addFields(config.fields);
  return tc.getInputType();
}

export function createResolver(config: any): Resolver<any, any> {
  const resolverOptions = Object.assign({}, config);
  if (resolverOptions.inputFields) {
    resolverOptions.args = {
      input: {
        description: '输入参数',
        type: createInputTypeComposer({
          name: config.name,
          fields: resolverOptions.inputFields
        })
      }
    };
    delete resolverOptions.inputFields;
  }
  let outputTC = null;
  if (config.outputTC) {
    outputTC = config.outputTC;
  } else if (config.outputFields) {
    outputTC = TypeComposer.create(config.name + 'Payload');
    outputTC.addFields(config.outputFields);
  }
  resolverOptions.type = outputTC;
  const rawResolve = resolverOptions.resolve;
  resolverOptions.resolve = ({source, args, context, projection, info}) => {
    context.user = context.req.user;
    if (context.user) {
      context.user.id = context.user.userId;
      context.userId = context.user.id;
    }
    return rawResolve(source, args, context, projection, info);
  };
  delete resolverOptions.outputFields;
  delete resolverOptions.outputTC;
  return new Resolver(resolverOptions);
}

export function addSystemToQuery(systemName: string, systemTypePrefix: string, description: string): TypeComposer<any> {
  const tc = TypeComposer.create(`${systemTypePrefix}Query`);
  tc.setDescription(description);
  const fields = {};
  fields[systemName] = {
    description,
    type: tc.getType(),
    resolve: () => {
      return {};
    }
  };
  GQC.rootQuery().addFields(fields);
  return tc;
}

export function addSystemToMutation(
  systemName: string,
  systemTypePrefix: string,
  description: string
): TypeComposer<any> {
  const tc = TypeComposer.create(`${systemTypePrefix}Mutation`);
  tc.setDescription(description);
  const fields = {};
  fields[systemName] = {
    description,
    type: tc.getType(),
    resolve: () => {
      return {};
    }
  };
  GQC.rootMutation().addFields(fields);
  return tc;
}

export function addCatalogToQuery(
  systemName: string,
  catalogName: string,
  catalogTypePrefix: string,
  description: string
): TypeComposer<any> {
  const tc = TypeComposer.create(`${catalogTypePrefix}Query`);
  tc.setDescription(description);
  const fields = {};
  fields[catalogName] = {
    description,
    type: tc.getType(),
    resolve: () => {
      return {};
    }
  };
  const SystemTC = GQC.rootQuery().get(systemName);
  SystemTC.addFields(fields);
  return tc;
}

export function addCatalogToMutation(
  systemName: string,
  catalogName: string,
  catalogTypePrefix: string,
  description: string
): TypeComposer<any> {
  const tc = TypeComposer.create(`${catalogTypePrefix}Mutation`);
  tc.setDescription(description);
  const fields = {};
  fields[catalogName] = {
    description,
    type: tc.getType(),
    resolve: () => {
      return {};
    }
  };
  const SystemTC = GQC.rootMutation().get(systemName);
  SystemTC.addFields(fields);
  return tc;
}

export {GQC, TypeComposer} from 'graphql-compose';
